package gen

import (
	"html/template"
	"io"
	"os"
	"path/filepath"
	"reflect"
	"strings"

	logging "github.com/ipfs/go-log"
	"golang.org/x/xerrors"
)

var log = logging.Logger("encoding.gen")

// TypeEncodingGenerator represents types that generate encoding/decoding impl for other types.
type TypeEncodingGenerator interface {
	// WriteImports outputs the imports.
	WriteImports(w io.Writer) error
	// WriteInit outputs the init for the file.
	WriteInit(w io.Writer, tis []TypeInfo) error
	// WriteEncodingForType outputs the encoding for the given type.
	WriteEncodingForType(w io.Writer, ti TypeInfo) error
}

// TypeInfo contains information about the a runtime type.
type TypeInfo struct {
	Name   string
	Fields []FieldInfo
}

// FieldInfo contains information about a field in a runtime type.
type FieldInfo struct {
	Name    string
	Pointer bool
	Type    reflect.Type
	Pkg     string

	IterLabel string
}

// WriteToFile creates a file and outputs all the generated encoding/decoding methods for the given types.
func WriteToFile(fname string, generator TypeEncodingGenerator, pkg string, types ...interface{}) error {
	log.Infof("Writing to file %s", fname)

	// create dir if needed
	err := os.MkdirAll(filepath.Dir(fname), os.ModePerm)
	if err != nil {
		return xerrors.Errorf("failed to create dir: %w", err)
	}

	// create file
	fi, err := os.Create(fname)
	if err != nil {
		return xerrors.Errorf("failed to open file: %w", err)
	}
	defer func() { _ = fi.Close() }()

	if err := writePackageHeader(fi, pkg); err != nil {
		return xerrors.Errorf("failed to write header: %w", err)
	}

	if err := generator.WriteImports(fi); err != nil {
		return xerrors.Errorf("failed to write generator imports: %w", err)
	}

	tis := []TypeInfo{}

	for _, t := range types {
		log.Debugf("Parsing type %T", t)
		ti, err := parseTypeInfo(pkg, t)
		if err != nil {
			return xerrors.Errorf("failed to parse type info: %w", err)
		}

		tis = append(tis, ti)
	}

	if err := generator.WriteInit(fi, tis); err != nil {
		return xerrors.Errorf("failed to write generator imports: %w", err)
	}

	for _, ti := range tis {
		if err := generator.WriteEncodingForType(fi, ti); err != nil {
			return xerrors.Errorf("failed to generate encoding methods: %w", err)
		}
	}

	return nil
}

func writePackageHeader(w io.Writer, pkg string) error {
	data := struct {
		Package string
	}{pkg}

	return doTemplate(w, data, `package {{ .Package }}
	
/* 
DO NOT EDIT THIS FILE BY HAND

This file was generated by "github.com/filecoin-project/go-filecoin/internal/pkg/encoding/gen" 
*/
`)
}

func parseTypeInfo(pkg string, i interface{}) (TypeInfo, error) {
	t := reflect.TypeOf(i)

	out := TypeInfo{
		Name: t.Name(),
	}

	// TODO: parse other types that are not structs

	for i := 0; i < t.NumField(); i++ {
		f := t.Field(i)
		if !nameIsExported(f.Name) {
			continue
		}

		ft := f.Type
		var pointer bool
		if ft.Kind() == reflect.Ptr {
			ft = ft.Elem()
			pointer = true
		}

		out.Fields = append(out.Fields, FieldInfo{
			Name:    "t." + f.Name,
			Pointer: pointer,
			Type:    ft,
			Pkg:     pkg,
		})
	}

	return out, nil
}

func doTemplate(w io.Writer, info interface{}, templ string) error {
	t := template.Must(template.New("").
		Funcs(template.FuncMap{}).Parse(templ))

	return t.Execute(w, info)
}

func nameIsExported(name string) bool {
	return strings.ToUpper(name[0:1]) == name[0:1]
}
